package scenic.geom;

import java.awt.Rectangle;
import java.awt.geom.AffineTransform;


/**
 * This class defines a 2-dimensional affine transform.
 * 
 */
public class Affine2 implements Cloneable {
	public double m00;
	public double m01;
	public double m10;
	public double m11;
	public double dx;
	public double dy;
	
	public Object clone() {
		Affine2 m = new Affine2();
		
		m.set(this);

		return m;
	}
	
	/**
	 * Sets the values of this transform to those of the given transform.
	 * 
	 * @param m the transform.
	 */
	public void set(Affine2 m) {
		m00 = m.m00;
		m01 = m.m01;
		m10 = m.m10;
		m11 = m.m11;
		dx = m.dx;
		dy = m.dy;	
	}
	
	/**
	 * Returns the dererminant of this affine tranform.
	 */
	public double determinant() {
		return m00 * m11 - m01 * m10;
	}
	
	/**
	 * Returns the inverse of this affine transform.
	 * 
	 * @return the inverse of this tranform.
	 */
	public Affine2 inverse() {
		Affine2 b = new Affine2();
		double det = m00 * m11 - m01* m10;

		b.m00 = m11 / det;
		b.m01 = -m01 / det;
		b.dx = -(b.m00 * dx + b.m01 * dy);

		b.m10 = -m10 / det;
		b.m11 = m00 / det;
		b.dy = -(b.m10 * dx + b.m11 * dy);

		return b;
	}

	/**
	 * Returns the AffineTranform object that corresponds to this object.
	 * 
	 * @return the AffineTranform object.
	 */
	public AffineTransform getAffineTransform() {
		return new AffineTransform(m00, m10, m01, m11, dx, dy);
	}
	
	/**
	 * Returns the AffineTranform object that corresponds to the linear
	 * transformation of this object. The linear transformation does
	 * not contain the displacement (dx, dy).
	 * 
	 * @return the AffineTranform object.
	 */
	public AffineTransform getLinearTransform() {
		return new AffineTransform(m00, m10, m01, m11, 0, 0);
	}

	/**
	 * Creates an identity transform.
	 * 
	 * @return the identity transform.
	 */
	public static Affine2 identity() {
		Affine2 m = new Affine2();
		
		m.m00 = 1;
		m.m11 = 1;
		
		return m;
	}
	
	/**
	 * Creates a translation transform.
	 * 
	 * @param dx the x coordinate of the translation.
	 * @param dy the y coordinate of the translation.
	 * @return the translation transform.
	 */
	public static Affine2 translate(double dx, double dy) {
		Affine2 m = identity();
		
		m.dx = dx;
		m.dy = dy;
		
		return m;
	}
	
	/**
	 * Creates a scaling transform.
	 * 
	 * @param x the x coordinate of the scaling.
	 * @param y the y coordinate of the scaling.
	 * @return the scaling transform.
	 */
	public static Affine2 scale(double x, double y) {
		Affine2 m = new Affine2();
		
		m.m00 = x;
		m.m11 = y;
		
		return m;		
	}
	
	/**
	 * Creates a rotation transform.
	 * 
	 * @param angle the angle of the rotation in degrees.
	 * @return the rotation transform.
	 */
	public static Affine2 rotate(double angle) {
	    Affine2 m = new Affine2();
	    double a = Math.toRadians(angle);

	    m.m00 = Math.cos(a);
	    m.m01 = -Math.sin(a);
	    m.m10 = -m.m01;
	    m.m11 = m.m00;
		
		return m;
	}
	
	/**
	 * Creates a transform that combines this transform with the
	 * given transform.
	 * 
	 * @param b the transform.
	 * @return the combined transform.
	 */
	public Affine2 multiply(Affine2 b) {
		Affine2 r = new Affine2();

		r.m00 = m00 * b.m00 + m01 * b.m10;
		r.m01 = m00 * b.m01 + m01 * b.m11;
		r.m10 = m10 * b.m00 + m11 * b.m10;
		r.m11 = m10 * b.m01 + m11 * b.m11;
		r.dx = m00 * b.dx + m01 * b.dy + dx;
		r.dy = m10 * b.dx + m11 * b.dy + dy;
		
		return r;		
	}
	
	/**
	 * Transforms the given point.
	 * 
	 * @param p the point to be transformed.
	 * @return the transformed point.
	 */
	public Double2 multiply(Double2 p) {
		Double2 r = new Double2();
		
		r.x = p.x * m00 + p.y * m01 + dx;
		r.y = p.x * m10 + p.y * m11 + dy;
		
		return r;
	}
	
	/**
	 * Creates a transform that combines the given transforms.
	 * 
	 * @param a the first transform.
	 * @param b the second transform.
	 * @return the combined transform.
	 */
	public static Affine2 multiply(Affine2 a, Affine2 b) {
		return a.multiply(b);
	}

	/**
	 * Creates a transform that corresponds to the given
	 * transform multiplied by the given scalar.
	 * 
	 * @param a the transform.
	 * @param m the scalar.
	 * @return the combined transform.
	 */
	public static Affine2 multiply(Affine2 a, double m) {
		Affine2 r = new Affine2();
		
		r.m00 = a.m00 * m;
		r.m01 = a.m01 * m;
		r.m10 = a.m10 * m;
		r.m11 = a.m11 * m;
		r.dx = a.dx * m;
		r.dy = a.dy * m;
		
		return r;
	}
	
	/**
	 * Returns a rectangle that contains the area of the given rectangle transformed
	 * by the given transform.
	 * 
	 * @param a the transform.
	 * @param b the rectangle.
	 * @return the combination.
	 */
	public static RectangleDouble transform(Affine2 a, RectangleDouble b) {
		RectangleDouble r = new RectangleDouble();

		r.x = a.m00 * ((a.m00 > 0) ? b.x : (b.x + b.width)) + 
				a.m01 * ((a.m01 > 0) ? b.y : (b.y + b.height)) +
				a.dx; 
		r.y = a.m10 * ((a.m10 > 0) ? b.x : (b.x + b.width)) +
			a.m11 * ((a.m11 > 0) ? b.y : (b.y + b.height)) +
			a.dy; 
		r.width = Math.abs(a.m00 * b.width) + Math.abs(a.m01 * b.height);
		r.height = Math.abs(a.m10 * b.width) + Math.abs(a.m11 * b.height);
			
		return r;
	}

	/**
	 * Returns a rectangle that contains the area of the given rectangle transformed
	 * by the given transform.
	 * 
	 * @param a the transform.
	 * @param b the rectangle.
	 * @return the combination.
	 */
	public static Rectangle transform(Affine2 a, Rectangle b) {
		Rectangle r = new Rectangle();

		r.x = (int)Math.floor(a.m00 * ((a.m00 > 0) ? b.x : (b.x + b.width)) + 
				a.m01 * ((a.m01 > 0) ? b.y : (b.y + b.height)) +
				a.dx); 
		r.y = (int)Math.floor(a.m10 * ((a.m10 > 0) ? b.x : (b.x + b.width)) +
			a.m11 * ((a.m11 > 0) ? b.y : (b.y + b.height)) +
			a.dy); 
		r.width = (int)Math.ceil(a.m00 * ((a.m00 < 0) ? b.x : (b.x + b.width)) + 
				a.m01 * ((a.m01 < 0) ? b.y : (b.y + b.height)) +
				a.dx) - r.x; 
		r.height = (int)Math.ceil(a.m10 * ((a.m10 < 0) ? b.x : (b.x + b.width)) +
			a.m11 * ((a.m11 < 0) ? b.y : (b.y + b.height)) +
			a.dy) - r.y; 
			
		return r;
	}

}
